/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import org.eclipse.imagen.util.CaselessStringKey;

/**
 * A class to manage the various instances of a descriptor factory. This also manages preferences between factory
 * instances for a specified descriptor/product.
 *
 * @since JAI 1.1
 */
class FactoryCache {

    /** * The registry mode name. */
    final String modeName;

    /** Cache the RegistryMode since it is bound to get used many, many times. */
    final RegistryMode mode;

    /** The Class corresponding to the factory. */
    final Class factoryClass;

    /** The name of the method in this factory used to do a "create" */
    final Method factoryMethod;

    /**
     * does the factory support preferences both among products and among multiple instances of the factory within the
     * same product ?
     */
    final boolean arePreferencesSupported;

    /** A Hashtable of all the instances, hashed by a filename that uniquely identifies each registered factory. */
    private Hashtable instances;

    /** A Hashtable of all the unique factory filenames, hashed by the factory they represent. */
    private Hashtable instancesByName;

    /** A count to give a number to each registered factory. */
    private int count = 0;

    /**
     * A Hashtable of a Hashtable of all the factory preferences, hashed by the descriptor name first and then the
     * product name that the factory belongs to. Each element of the per product Hashtable is a Vector which contains a
     * list of pairwise factory preferences stored as Vectors.
     */
    private Hashtable prefs;

    /**
     * Constructor. Create a FactoryCache to hold factory objects for a specific mode.
     *
     * @param modeName the registry mode name.
     */
    FactoryCache(String modeName) {

        this.modeName = modeName;

        mode = RegistryMode.getMode(modeName);
        factoryClass = mode.getFactoryClass();
        factoryMethod = mode.getFactoryMethod();
        arePreferencesSupported = mode.arePreferencesSupported();

        instances = new Hashtable();

        if (arePreferencesSupported) {
            instancesByName = new Hashtable();
            prefs = new Hashtable();
        }
    }

    /**
     * Invoke the create method of the given factory instance
     *
     * @param factoryInstance an instance of this factory
     * @param parameterValues the parameterValues to be passed in to the the create method.
     * @return the object created by the create method
     * @throws IllegalArgumentException thrown by Method.invoke
     * @throws InvocationTargetException thrown by Method.invoke
     * @throws IllegalAccessException thrown by Method.invoke
     */
    Object invoke(Object factoryInstance, Object[] parameterValues)
            throws InvocationTargetException, IllegalAccessException {

        return factoryMethod.invoke(factoryInstance, parameterValues);
    }

    /**
     * Add a factory instance to this factory. If the factory has NO preferences add it to a table hashed by just the
     * operation name. Otherwise add it to two tables, one hashed by a unique filename (modeName + count) and the other
     * hashed by the factory interface name.
     *
     * @param descriptorName operation that this factory instance implements
     * @param productName product to which this factory instance belongs
     * @param factoryInstance the factory instance
     */
    void addFactory(String descriptorName, String productName, Object factoryInstance) {

        checkInstance(factoryInstance);

        if (arePreferencesSupported) {

            if (productName == null) throw new IllegalArgumentException(JaiI18N.getString("Generic0"));

            // Update structures to reflect the addition of
            // this factory instance.
            Vector v = new Vector();

            v.add(factoryInstance.getClass().getName());
            v.add(productName);
            v.add(descriptorName);

            CaselessStringKey fileName = new CaselessStringKey(modeName + count);

            instancesByName.put(factoryInstance, fileName);
            instances.put(fileName, v);
            count++;

        } else instances.put(new CaselessStringKey(descriptorName), factoryInstance);
    }

    /**
     * Remove a facory instance associated with the specified operation
     *
     * @param descriptorName operation that this factory instance implements
     * @param productName product to which this factory instance belongs
     * @param factoryInstance the factory instance
     */
    void removeFactory(String descriptorName, String productName, Object factoryInstance) {

        checkInstance(factoryInstance);
        checkRegistered(descriptorName, productName, factoryInstance);

        if (arePreferencesSupported) {

            // Update structures to reflect the removal of
            // this factoryInstance.
            CaselessStringKey fileName = (CaselessStringKey) instancesByName.get(factoryInstance);

            instancesByName.remove(factoryInstance);
            instances.remove(fileName);
            count--;
        } else {
            instances.remove(new CaselessStringKey(descriptorName));
        }
    }

    /**
     * Sets a preference between two factory instances for the given operation and product.
     *
     * @param descriptorName operation that this factory instance implements
     * @param productName product to which this factory instance belongs
     * @param preferredOp the preferred factory instance
     * @param otherOp the not-so preferred/other factory instance
     */
    void setPreference(String descriptorName, String productName, Object preferredOp, Object otherOp) {

        if (!arePreferencesSupported) {
            throw new IllegalArgumentException(JaiI18N.formatMsg("FactoryCache1", new Object[] {modeName}));
        }

        if ((preferredOp == null) || (otherOp == null)) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        checkRegistered(descriptorName, productName, preferredOp);
        checkRegistered(descriptorName, productName, otherOp);

        if (preferredOp == otherOp) return;

        checkInstance(preferredOp);
        checkInstance(otherOp);

        CaselessStringKey dn = new CaselessStringKey(descriptorName);
        CaselessStringKey pn = new CaselessStringKey(productName);

        Hashtable dht = (Hashtable) prefs.get(dn);

        if (dht == null) {
            prefs.put(dn, dht = new Hashtable());
        }

        Vector pv = (Vector) dht.get(pn);

        if (pv == null) {
            dht.put(pn, pv = new Vector());
        }

        pv.addElement(new Object[] {preferredOp, otherOp});
    }

    /**
     * Unets a preference between two factory instances for the given operation and product.
     *
     * @param descriptorName operation that this factory instance implements
     * @param productName product to which this factory instance belongs
     * @param preferredOp the preferred factory instance
     * @param otherOp the not-so preferred/other factory instance
     */
    void unsetPreference(String descriptorName, String productName, Object preferredOp, Object otherOp) {

        if (!arePreferencesSupported) {
            throw new IllegalArgumentException(JaiI18N.formatMsg("FactoryCache1", new Object[] {modeName}));
        }

        if ((preferredOp == null) || (otherOp == null)) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        checkRegistered(descriptorName, productName, preferredOp);
        checkRegistered(descriptorName, productName, otherOp);

        if (preferredOp == otherOp) return;

        checkInstance(preferredOp);
        checkInstance(otherOp);

        // Update structures to reflect removal of this pref.
        Hashtable dht = (Hashtable) prefs.get(new CaselessStringKey(descriptorName));

        boolean found = false;

        if (dht != null) {

            Vector pv = (Vector) dht.get(new CaselessStringKey(productName));

            if (pv != null) {
                Iterator it = pv.iterator();

                while (it.hasNext()) {
                    Object[] objs = (Object[]) it.next();

                    if ((objs[0] == preferredOp) && (objs[1] == otherOp)) {

                        it.remove();
                        found = true;
                    }
                }
            }
        }

        if (!found)
            throw new IllegalArgumentException(JaiI18N.formatMsg("FactoryCache2", new Object[] {
                preferredOp.getClass().getName(), otherOp.getClass().getName(), modeName, descriptorName, productName
            }));
    }

    /**
     * Gets an iterator over all preferences set between factory instances for a given descriptor and product.
     *
     * @param descriptorName operation that this factory instance implements
     * @param productName product to which this factory instance belongs
     */
    Object[][] getPreferences(String descriptorName, String productName) {

        if (!arePreferencesSupported) {
            throw new IllegalArgumentException(JaiI18N.formatMsg("FactoryCache1", new Object[] {modeName}));
        }

        if ((descriptorName == null) || (productName == null))
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));

        // Update structures to reflect removal of this pref.
        Hashtable dht = (Hashtable) prefs.get(new CaselessStringKey(descriptorName));

        if (dht != null) {

            Vector pv = (Vector) dht.get(new CaselessStringKey(productName));

            if (pv != null) {
                return (Object[][]) pv.toArray(new Object[0][]);
            }
        }

        return null;
    }

    /**
     * Removes all preferences set between factory instances for a given descriptor and product.
     *
     * @param descriptorName operation that this factory instance implements
     * @param productName product to which this factory instance belongs
     */
    void clearPreferences(String descriptorName, String productName) {

        if (!arePreferencesSupported) {
            throw new IllegalArgumentException(JaiI18N.formatMsg("FactoryCache1", new Object[] {modeName}));
        }

        // Update structures to reflect removal of this pref.
        Hashtable dht = (Hashtable) prefs.get(new CaselessStringKey(descriptorName));

        if (dht != null) dht.remove(new CaselessStringKey(productName));
    }

    /** Get a list of factory objects registered under the descriptor and the product (in no particular order). */
    List getFactoryList(String descriptorName, String productName) {

        if (arePreferencesSupported) {

            ArrayList list = new ArrayList();

            Enumeration keys = instancesByName.keys();

            while (keys.hasMoreElements()) {
                Object instance = keys.nextElement();
                CaselessStringKey fileName = (CaselessStringKey) instancesByName.get(instance);

                Vector v = (Vector) instances.get(fileName);

                String dn = (String) v.get(2);
                String pn = (String) v.get(1);

                if (descriptorName.equalsIgnoreCase(dn) && productName.equalsIgnoreCase(pn)) list.add(instance);
            }

            return list;

        } else {
            Object obj = instances.get(new CaselessStringKey(descriptorName));

            ArrayList list = new ArrayList(1);

            list.add(obj);
            return list;
        }
    }

    /** Get the local name of a factoryInstance */
    String getLocalName(Object factoryInstance) {
        CaselessStringKey fileName = (CaselessStringKey) instancesByName.get(factoryInstance);

        if (fileName != null) return fileName.getName();

        return null;
    }

    /** Check to see if the factoryInstance is valid object of this registry mode. */
    private boolean checkInstance(Object factoryInstance) {

        if (!factoryClass.isInstance(factoryInstance))
            throw new IllegalArgumentException(JaiI18N.formatMsg(
                    "FactoryCache0",
                    new Object[] {factoryInstance.getClass().getName(), modeName, factoryClass.getName()}));

        return true;
    }

    /**
     * Check to see if <code>factoryInstance</code> is registered against the specified <code>descriptorName</code> and
     * <code>productName</code>.
     */
    private void checkRegistered(String descriptorName, String productName, Object factoryInstance) {

        if (arePreferencesSupported) {

            if (productName == null)
                throw new IllegalArgumentException("productName : " + JaiI18N.getString("Generic0"));

            CaselessStringKey fileName = (CaselessStringKey) instancesByName.get(factoryInstance);

            if (fileName != null) {

                Vector v = (Vector) instances.get(fileName);

                String pn = (String) v.get(1);
                String dn = (String) v.get(2);

                if ((dn != null)
                        && dn.equalsIgnoreCase(descriptorName)
                        && (pn != null)
                        && pn.equalsIgnoreCase(productName)) {
                    return;
                }
            }

            throw new IllegalArgumentException(JaiI18N.formatMsg(
                    "FactoryCache3", new Object[] {factoryInstance.getClass().getName(), descriptorName, productName}));
        } else {

            CaselessStringKey key = new CaselessStringKey(descriptorName);

            if (factoryInstance != instances.get(key)) {
                throw new IllegalArgumentException(JaiI18N.formatMsg(
                        "FactoryCache4",
                        new Object[] {factoryInstance.getClass().getName(), descriptorName}));
            }
        }
    }
}
